package org.jetbrains.kotlin

import groovy.lang.Closure
import org.gradle.api.Action
import org.gradle.api.DefaultTask
import org.gradle.api.Task
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction

import org.jetbrains.kotlin.konan.target.*

import java.io.FileWriter
import java.nio.file.Path
import java.nio.file.Paths

/**
 * Test task for -produce framework testing. Requires a framework to be built by the Konan plugin
 * with konanArtifacts { framework(frameworkName, targets: [ testTarget] ) } and a dependency set
 * according to a pattern "compileKonan${frameworkName}".
 *
 * @property swiftSources  Swift-language test sources that use a given framework
 * @property testName test name
 * @property frameworkNames names of frameworks
 */
open class FrameworkTest : DefaultTask() {
    @Input
    lateinit var swiftSources: List<String>

    @Input
    lateinit var testName: String

    @Input
    lateinit var frameworkNames: List<String>

    @Input
    var fullBitcode: Boolean = false

    private val testOutput: String = project.testOutputFramework

    override fun configure(config: Closure<*>): Task {
        super.configure(config)
        val target = project.testTarget.name

        // set crossdist build dependency if custom konan.home wasn't set
        if (!(project.property("useCustomDist") as Boolean)) {
            setRootDependency("${target}CrossDist", "${target}CrossDistRuntime", "commonDistRuntime", "distCompiler")
        }
        check(::testName.isInitialized) { "Test name should be set" }
        check(::frameworkNames.isInitialized) { "Framework names should be set" }
        frameworkNames.forEach { frameworkName ->
            dependsOn(project.tasks.getByName("compileKonan$frameworkName"))
        }
        return this
    }

    private fun setRootDependency(vararg s: String) = s.forEach { dependsOn(project.rootProject.tasks.getByName(it)) }

    @TaskAction
    fun run() {
        val frameworkParentDirPath = "$testOutput/$testName/${project.testTarget.name}"
        frameworkNames.forEach { frameworkName ->
            val frameworkPath = "$frameworkParentDirPath/$frameworkName.framework"
            val frameworkBinaryPath = "$frameworkPath/$frameworkName"
            validateBitcodeEmbedding(frameworkBinaryPath)
            codesign(project, frameworkPath)
        }

        // create a test provider and get main entry point
        val provider = Paths.get(testOutput, testName, "provider.swift")
        FileWriter(provider.toFile()).use {
            it.write("""
                |// THIS IS AUTOGENERATED FILE
                |// This method is invoked by the main routine to get a list of tests
                |func registerProvider() {
                |    // TODO: assuming this naming for now
                |    ${testName}Tests()
                |}
                """.trimMargin())
        }
        val testHome = project.file("framework").toPath()
        val swiftMain = Paths.get(testHome.toString(), "main.swift").toString()

        // Compile swift sources
        val sources = swiftSources.map { Paths.get(it).toString() } +
                listOf(provider.toString(), swiftMain)
        val options = listOf(
                "-g",
                "-Xlinker", "-rpath", "-Xlinker", frameworkParentDirPath,
                "-F", frameworkParentDirPath,
                "-Xcc", "-Werror" // To fail compilation on warnings in framework header.
        )
        val testExecutable = Paths.get(testOutput, testName, "swiftTestExecutable")
        compileSwift(project, project.testTarget, sources, options, testExecutable, fullBitcode)

        runTest(testExecutable)
    }

    /**
     * Returns path to directory that contains `libswiftCore.dylib` for the current
     * test target.
     */
    private fun getSwiftLibsPathForTestTarget(): String {
        val target = project.testTarget
        val platform = project.platformManager.platform(target)
        val configs = platform.configurables as AppleConfigurables
        val swiftPlatform = when (target) {
            KonanTarget.IOS_X64 -> "iphonesimulator"
            KonanTarget.IOS_ARM32, KonanTarget.IOS_ARM64 -> "iphoneos"
            KonanTarget.TVOS_X64 -> "appletvsimulator"
            KonanTarget.TVOS_ARM64 -> "appletvos"
            KonanTarget.MACOS_X64 -> "macosx"
            KonanTarget.WATCHOS_ARM64 -> "watchos"
            KonanTarget.WATCHOS_X64, KonanTarget.WATCHOS_X86 -> "watchsimulator"
            else -> throw IllegalStateException("Test target $target is not supported")
        }
        val simulatorPath = when (target) {
            KonanTarget.TVOS_X64,
            KonanTarget.IOS_X64,
            KonanTarget.WATCHOS_X86,
            KonanTarget.WATCHOS_X64 -> Xcode.current.getLatestSimulatorRuntimeFor(target, configs.osVersionMin)?.bundlePath?.let {
                "$it/Contents/Resources/RuntimeRoot/usr/lib/swift"
            }
            else -> null
        }
        // Use default path from toolchain if we cannot get `bundlePath` for target.
        // It may be the case for simulators if Xcode/macOS is old.
        return simulatorPath ?: configs.absoluteTargetToolchain + "/usr/lib/swift-5.0/$swiftPlatform"
    }

    private fun buildEnvironment(): Map<String, String> {
        val target = project.testTarget
        // Hopefully, lexicographical comparison will work.
        val newMacos = System.getProperty("os.version").compareTo("10.14.4") >= 0
        val dyldLibraryPathKey = when (target) {
            KonanTarget.IOS_X64,
            KonanTarget.WATCHOS_X86,
            KonanTarget.WATCHOS_X64,
            KonanTarget.TVOS_X64 -> "SIMCTL_CHILD_DYLD_LIBRARY_PATH"
            else -> "DYLD_LIBRARY_PATH"
        }
        return if (newMacos && target == KonanTarget.MACOS_X64) emptyMap() else mapOf(
                dyldLibraryPathKey to getSwiftLibsPathForTestTarget()
        )
    }

    private fun runTest(testExecutable: Path, args: List<String> = emptyList()) {
        val executor = (project.convention.plugins["executor"] as? ExecutorService)
                ?: throw RuntimeException("Executor wasn't found")
        val (stdOut, stdErr, exitCode) = runProcess(
                executor = executor.add(Action { it.environment = buildEnvironment() })::execute,
                executable = testExecutable.toString(),
                args = args)

        val testExecName = testExecutable.fileName
        println("""
            |$testExecName
            |stdout: $stdOut
            |stderr: $stdErr
            """.trimMargin())
        check(exitCode == 0) { "Execution of $testExecName failed with exit code: $exitCode " }
    }

    private fun validateBitcodeEmbedding(frameworkBinary: String) {
        // Check only the full bitcode embedding for now.
        if (!fullBitcode) {
            return
        }
        val testTarget = project.testTarget
        val configurables = project.platformManager.platform(testTarget).configurables as AppleConfigurables

        val bitcodeBuildTool = Paths.get("${configurables.absoluteAdditionalToolsDir}/bin/bitcode-build-tool")
        val toolPath = "${configurables.absoluteTargetToolchain}/usr/bin/"
        val sdk = when (testTarget) {
            KonanTarget.IOS_X64,
            KonanTarget.TVOS_X64,
            KonanTarget.WATCHOS_X86,
            KonanTarget.WATCHOS_X64 -> return // bitcode-build-tool doesn't support simulators.
            KonanTarget.IOS_ARM64,
            KonanTarget.IOS_ARM32 -> Xcode.current.iphoneosSdk
            KonanTarget.MACOS_X64 -> Xcode.current.macosxSdk
            KonanTarget.TVOS_ARM64 -> Xcode.current.appletvosSdk
            KonanTarget.WATCHOS_ARM32,
            KonanTarget.WATCHOS_ARM64 -> Xcode.current.watchosSdk
            else -> error("Cannot validate bitcode for test target $testTarget")
        }

        runTest(bitcodeBuildTool, args = listOf("--sdk", sdk, "-v", "-t", toolPath, frameworkBinary))
    }
}